﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace Spanner
{
    public class Expr<T> : ITerm
    {
        // We implicitly cast constants and structures to Exprs.
        public static implicit operator Expr<T>(T x) { return new ConstExpr<T> { Const = x }; }

        // ObsVarRefs are common and it's convenient not to have to explicitly Deref them
        // (although sometimes it's unavoidable and you have to help C# out with a Deref).
        public static implicit operator Expr<T>(Expr<ObsVarRef<T>> x) { return UI.Deref(x); }

        // Utilities since we override == and !=.
        internal static bool IsNull(Expr<T> e) { return object.ReferenceEquals(e, null); }
        internal static bool IsNotNull(Expr<T> e) { return !IsNull(e); }

        // Note that & and | correspond to && and ||.  C# has some odd rules which mean you can't overload && and ||
        // (they are translated differently by the C# compiler to preserve short-circuit semantics).
        public static Expr<bool> operator <(Expr<T> x, Expr<T> y) {
            return new InfixExpr<bool> { LHS = x, Op = "<", RHS = y };
        }
        public static Expr<bool> operator <=(Expr<T> x, Expr<T> y) {
            return new InfixExpr<bool> { LHS = x, Op = "<=", RHS = y };
        }
        public static Expr<bool> operator ==(Expr<T> x, Expr<T> y) {
            return new InfixExpr<bool> { LHS = x, Op = "===", RHS = y };
        }
        public static Expr<bool> operator !=(Expr<T> x, Expr<T> y) {
            return new InfixExpr<bool> { LHS = x, Op = "!==", RHS = y };
        }
        public static Expr<bool> operator >=(Expr<T> x, Expr<T> y) {
            return new InfixExpr<bool> { LHS = x, Op = ">=", RHS = y };
        }
        public static Expr<bool> operator >(Expr<T> x, Expr<T> y) {
            return new InfixExpr<bool> { LHS = x, Op = ">", RHS = y };
        }
        public static Expr<bool> operator &(Expr<T> x, Expr<T> y) {
            B("&"); return new InfixExpr<bool> { LHS = x, Op = "&&", RHS = y };
        }
        public static Expr<bool> operator |(Expr<T> x, Expr<T> y) {
            B("|"); return new InfixExpr<bool> { LHS = x, Op = "||", RHS = y };
        }
        public static Expr<bool> operator !(Expr<T> x) {
            B("!"); return new PrefixExpr<bool> { Op = "!", RHS = x };
        }
        public static Expr<T> operator +(Expr<T> x, Expr<T> y) {
            IorDorS("+"); return new InfixExpr<T> { LHS = x, Op = "+", RHS = y };
        }
        public static Expr<T> operator +(Expr<T> x) {
            IorD("+"); return new PrefixExpr<T> { Op = " + ", RHS = x };
        }
        public static Expr<T> operator -(Expr<T> x, Expr<T> y) {
            IorD("-"); return new InfixExpr<T> { LHS = x, Op = "-", RHS = y };
        }
        public static Expr<T> operator -(Expr<T> x) {
            IorD("-"); return new PrefixExpr<T> { Op = " - ", RHS = x };
        }
        public static Expr<T> operator *(Expr<T> x, Expr<T> y) {
            IorD("*"); return new InfixExpr<T> { LHS = x, Op = "*", RHS = y };
        }
        public static Expr<T> operator /(Expr<T> x, Expr<T> y) {
            IorD("/"); return new InfixExpr<T> { LHS = x, Op = "/", RHS = y };
        }
        public static Expr<T> operator %(Expr<T> x, Expr<T> y) {
            I("%"); return new InfixExpr<T> { LHS = x, Op = "%", RHS = y };
        }

        //public FieldExpr<ObsVarRef<Tf>> Field<Tf>(Expression<Func<T, ObsVar<Tf>>> f)
        //{
        //    return new FieldExpr<ObsVarRef<Tf>> { X = this, Fields = FieldExpressionToAccessString(f) };
        //}

        public FieldExpr<Tf> Field<Tf>(Expression<Func<T, Expr<Tf>>> f)
        {
            return new FieldExpr<Tf> { X = this, Fields = FieldExpressionToAccessString(f) };
        }

        public FieldExpr<Tf> Field<Tf>(Expression<Func<T, Tf>> f)
        {
            return new FieldExpr<Tf> { X = this, Fields = FieldExpressionToAccessString(f) };
        }

        public ObsVarField<Tf> ObsVarField<Tf>(Expression<Func<T, ObsVar<Tf>>> f)
        {
            return new ObsVarField<Tf> { X = this, Fields = FieldExpressionToAccessString(f) };
        }

        internal static string FieldExpressionToAccessString<To, Tf>(Expression<Func<To, Tf>> f)
        {
            var lambda = (LambdaExpression)f;
            var param = f.Parameters.First();
            var fields = "";
            var body = f.Body as MemberExpression;
            var expr = null as MemberExpression;
            while (body != null)
            {
                fields = "." + body.Member.Name + fields;
                expr = body.Expression as MemberExpression;
                if (expr == null) break;
                body = expr;
            }
            return fields;
        }

        private static void IorDorS(string op)
        {
            var t = typeof(T);
            if (t == typeof(int) || t == typeof(double) || t == typeof(string)) return;
            throw new ApplicationException(op + " only applies to Expr<T>s where T is int or double or string.");
        }

        private static void IorD(string op)
        {
            var t = typeof(T);
            if (t == typeof(int) || t == typeof(double)) return;
            throw new ApplicationException(op + " only applies to Expr<T>s where T is int or double.");
        }

        private static void I(string op)
        {
            if (typeof(T) == typeof(int)) return;
            throw new ApplicationException(op + " only applies to Expr<int>s.");
        }

        private static void B(string op)
        {
            if (typeof(T) == typeof(bool)) return;
            throw new ApplicationException(op + " only applies to Expr<bool>s.");
        }

        // Needed because we define == and !=.
        public override int GetHashCode()
        {
            return base.GetHashCode();
        }

        // Needed because we define == and !=.
        public override bool Equals(object o)
        {
            return base.Equals(o);
        }

        void ITerm.Write(Context ctxt, JSorHtml what) { Write(ctxt, what); }

        internal virtual void Write(Context ctxt, JSorHtml what) { }

        IEnumerable<IVar> ITerm.Dependencies() { return Dependencies(); }

        internal virtual IEnumerable<IVar> Dependencies() { yield break; }

        internal IEnumerable<IVar> DependenciesOfArbitraryTerm(object o, HashSet<object> visited = null)
        {
            if (o == null || o is char || o is string || o is int || o is double || o is bool)
            {
                yield break;
            }

            // There's always somebody who builds cyclic data structures...

            if (visited != null && visited.Contains(o)) yield break; // Don't go in circles!

            if (visited == null) visited = new HashSet<object> { o }; else visited.Add(o);

            var v = o as IVar;
            if (v != null)
            {
                yield return v;
                yield break;
            }

            var e = o as ITerm;
            if (e != null) {
                foreach (var x in e.Dependencies()) yield return x;
                yield break;
            }

            var xs = o as IEnumerable;
            if (xs != null) {
                foreach (var x in xs) foreach (var y in DependenciesOfArbitraryTerm(x, visited)) yield return y;
                yield break;
            }

            var oType = o.GetType();
            var fields = oType.GetFields();
            var properties = oType.GetProperties().Where(x => x.CanRead);
            foreach (var field in fields)
                foreach (var x in DependenciesOfArbitraryTerm(field.GetValue(o), visited))
                    yield return x;
            foreach (var property in properties)
                foreach (var x in DependenciesOfArbitraryTerm(property.GetValue(o), visited))
                    yield return x;
        }

        internal void WriteArbitraryTerm(Context ctxt, JSorHtml what, object o)
        {
            var sep = (string)null;

            if (o == null) {
                ctxt.Write("null");
                return;
            }

            var e = o as ITerm;
            if (e != null)
            {
                e.Write(ctxt, what);
                return;
            }

            if (o is char || o is string || o.GetType().IsEnum) {
                ctxt.Write("\"" + o.ToString() + "\"");
                return;
            }

            if (o is int || o is double) {
                ctxt.Write(o.ToString());
                return;
            }

            if (o is bool) {
                ctxt.Write((bool)o ? "true" : "false");
                return;
            }

            var xs = o as IEnumerable;
            if (xs != null)
            {
                ctxt.Write("[");
                ctxt.Indentation++;
                sep = "";
                foreach (var x in xs)
                {
                    ctxt.Write(sep);
                    WriteArbitraryTerm(ctxt, what, x);
                    sep = ", ";
                }
                ctxt.Write("]");
                ctxt.Indentation--;
                return;
            }

            // Otherwise this is some other kind of object.  We'll write it out in JSON form.

            var fields = o.GetType().GetFields();
            var properties = o.GetType().GetProperties().Where(x => x.CanRead);

            Action<string, object> writeField = (k, v) =>
            {
                ctxt.Write(sep); sep = ", ";
                ctxt.Write(k);
                ctxt.Write(":");
                var iExpr = v as ITerm;
                if (iExpr != null)
                {
                    iExpr.Write(ctxt, what);
                }
                else
                {
                    WriteArbitraryTerm(ctxt, what, v);
                }
            };

            sep = "";
            ctxt.Write("{{");
            foreach (var field in fields) writeField(field.Name, field.GetValue(o));
            foreach (var property in properties) writeField(property.Name, property.GetValue(o));
            ctxt.Write("}}");
        }
    }

    internal class ConstExpr<T> : Expr<T>
    {
        internal object Const;

        internal override IEnumerable<IVar> Dependencies()
        {
            return DependenciesOfArbitraryTerm(Const);
        }

        internal override void Write(Context ctxt, JSorHtml what)
        {
            WriteArbitraryTerm(ctxt, what, Const);
        }
    }

    internal class InfixExpr<T> : Expr<T>
    {
        internal ITerm LHS;
        internal string Op;
        internal ITerm RHS;

        internal override IEnumerable<IVar> Dependencies()
        {
            if ((object)LHS != null)
            {
                if (LHS is IVar)
                    yield return (IVar)LHS;
                else
                    foreach (var x in LHS.Dependencies()) yield return x;
            }
            if ((object)RHS != null)
            {
                if (RHS is IVar)
                    yield return (IVar)RHS;
                else
                    foreach (var x in RHS.Dependencies()) yield return x;
            }
        }

        internal override void Write(Context ctxt, JSorHtml what)
        {
            ctxt.WritePlain("(");
            if ((object)LHS == null) ctxt.WritePlain("null"); else LHS.Write(ctxt, what);
            ctxt.WritePlain(" " + Op + " ");
            if ((object)RHS == null) ctxt.WritePlain("null"); else RHS.Write(ctxt, what);
            ctxt.WritePlain(")");
        }
    }

    internal class PrefixExpr<T> : Expr<T>
    {
        internal string Op;
        internal ITerm RHS;

        internal override IEnumerable<IVar> Dependencies()
        {
            if ((object)RHS != null)
            {
                if (RHS is IVar)
                    yield return (IVar)RHS;
                else
                    foreach (var x in RHS.Dependencies()) yield return x;
            }
        }

        internal override void Write(Context ctxt, JSorHtml what)
        {
            ctxt.WritePlain(Op);
            if ((object)RHS == null) ctxt.WritePlain("null"); else RHS.Write(ctxt, what);
        }
    }

    public class FieldExpr<T> : Expr<T>, ITerm
    {
        internal ITerm X;
        internal string Fields;

        void ITerm.Write(Context ctxt, JSorHtml what)
        {
            ctxt.WriteTerm((ITerm)X, what);
            ctxt.Write(Fields);
            var typeOfT = typeof(T);
            var genericTypeOfT = (typeOfT.IsGenericType ? typeOfT.GetGenericTypeDefinition() : null);
            if (genericTypeOfT == typeof(ObsVar<>) || genericTypeOfT == typeof(ComputedVar<>)) ctxt.Write("()");
        }

        IEnumerable<IVar> ITerm.Dependencies()
        {
            return ((ITerm)X).Dependencies();
        }
    }

    public class IndexExpr<T> : Expr<T>, ITerm
    {
        internal Expr<T[]> X;
        internal Expr<int> I;

        void ITerm.Write(Context ctxt, JSorHtml what)
        {
            ctxt.WriteTerm((ITerm)X, what);
            ctxt.Write("[", what);
            ctxt.WriteTerm((ITerm)I, what);
            ctxt.Write("]", what);
        }

        IEnumerable<IVar> ITerm.Dependencies()
        {
            return ((ITerm)X).Dependencies().Concat(((ITerm)I).Dependencies());
        }
    }

    internal class FmtExpr<T> : Expr<T>, ITerm
    {
        internal string Desc; // Just for documentation.

        internal ITerm[] Children;

        internal FmtExpr(string desc, params ITerm[] children) { Desc = desc; Children = children; }

        void ITerm.Write(Context ctxt, JSorHtml what)
        {
            foreach (var x in Children) x.Write(ctxt, what);
        }

        IEnumerable<IVar> ITerm.Dependencies()
        {
            return Children.SelectMany(x => (x is IVar) ? new IVar[] { (IVar)x } : ((ITerm)x).Dependencies());
        }
    }

    internal class JSExpr<T> : Expr<T>, ITerm
    {
        internal FmtOneLiner Fmt;

        internal JSExpr(string fmt, params ITerm[] args)
        {
            Fmt = new FmtOneLiner { Fmt = fmt, Children = args };
        }

        void ITerm.Write(Context ctxt, JSorHtml what) { ctxt.WriteTerm((ITerm)Fmt, what); }

        IEnumerable<IVar> ITerm.Dependencies() { return ((ITerm)Fmt).Dependencies(); }
    }

    public class ObsVarRef<T> : Expr<ObsVarRef<T>>, ITerm
    {
        internal AbstractVar<T> Ref;

        void ITerm.Write(Context ctxt, JSorHtml what)
        {
            ctxt.WriteIVar(what, Ref, deref: false);
        }

        IEnumerable<IVar> ITerm.Dependencies()
        {
            return new IVar[] { Ref };
        }
    }

    public class ComputedVarRef<T> : Expr<ComputedVarRef<T>>, ITerm
    {
        internal AbstractVar<T> Ref;

        void ITerm.Write(Context ctxt, JSorHtml what)
        {
            ctxt.WriteIVar(what, Ref, deref: false);
        }

        IEnumerable<IVar> ITerm.Dependencies()
        {
            return new IVar[] { Ref };
        }
    }
}
